﻿<!DOCTYPE html>
<html>
<head>
  <title>Timeline</title>
  <!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
  <meta name="description" content="A stretchable timeline." />
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="../release/go.js"></script>
  <script src="../assets/js/goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
  <script id="code">
    function init() {
      if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this

      var $ = go.GraphObject.make;  // for conciseness in defining templates

      myDiagram = $(go.Diagram, "myDiagramDiv",  // create a Diagram for the DIV HTML element
        {
          "animationManager.isEnabled": false,
          "commandHandler.decreaseZoom": function() { changeScale(1/1.05) },
          "commandHandler.increaseZoom": function() { changeScale(1.05) },
          "commandHandler.resetZoom": function() { setScale(1.0) },
          layout: $(TimelineLayout),
          isTreePathToChildren: false  // arrows from children (events) to the parent (timeline bar)
        });

      function changeScale(factor) {
        var oldscale = myDiagram.model.modelData.scale || 1.0;
        var newscale = factor ? (oldscale * factor) : 1.0;
        setScale(newscale);
      }

      function setScale(scale) {
        var docpt = myDiagram.lastInput.documentPoint.copy();
        var line = null;
        myDiagram.commit(function(diag) {
          diag.model.set(diag.model.modelData, "scale", scale);
          diag.nodes.each(function(n) {
            if (n.category === "Line") {
              line = n;
              n.updateTargetBindings();
              return;
            }
          });
        }, null);  // no UndoManager
        if (line !== null && docpt.x > line.position.x) {
          myDiagram.position = new go.Point(docpt.x - (docpt.x-line.position.x)/scale, myDiagram.position.y);
        }
      }

      myDiagram.nodeTemplate =
        $(go.Node, "Table",
          { locationSpot: go.Spot.Center, movable: false },
          $(go.Panel, "Auto",
            $(go.Shape, "RoundedRectangle",
              { fill: "#252526", stroke: "#519ABA", strokeWidth: 3 }
            ),
            $(go.Panel, "Table",
              $(go.TextBlock,
                {
                  row: 0,
                  stroke: "#CCCCCC",
                  wrap: go.TextBlock.WrapFit,
                  font: "bold 12pt sans-serif",
                  textAlign: "center", margin: 4
                },
                new go.Binding("text", "event")
              ),
              $(go.TextBlock,
                {
                  row: 1,
                  stroke: "#A074C4",
                  textAlign: "center", margin: 4
                },
                new go.Binding("text", "date", function(d) { return d.toLocaleDateString(); })
              )
            )
          )
        );

      myDiagram.nodeTemplateMap.add("Line",
        $(go.Node, "Graduated",
          {
            movable: false, copyable: false,
            resizable: true, resizeObjectName: "MAIN",
            background: "transparent",
            graduatedMin: 0,
            graduatedMax: 365,
            graduatedTickUnit: 1,
            resizeAdornmentTemplate:  // only resizing at right end
              $(go.Adornment, "Spot",
                $(go.Placeholder),
                $(go.Shape, { alignment: go.Spot.Right, cursor: "e-resize", desiredSize: new go.Size(4, 16), fill: "lightblue", stroke: "deepskyblue" })
              )
          },
          new go.Binding("graduatedMax", "", timelineDays),
          $(go.Shape, "LineH",
            { name: "MAIN", stroke: "#519ABA", height: 1, strokeWidth: 3 },
            new go.Binding("width", "length", function(l, shape) { return l * (shape.diagram.model.modelData.scale || 1.0); })
              .makeTwoWay(function(w, data, model) { return w/model.modelData.scale; })
          ),
          $(go.Shape, { geometryString: "M0 0 V10", interval: 7, stroke: "#519ABA", strokeWidth: 2 }),
          $(go.TextBlock,
            {
              font: "10pt sans-serif",
              stroke: "#CCCCCC",
              interval: 14,
              alignmentFocus: go.Spot.Right,
              segmentOrientation: go.Link.OrientMinus90,
              segmentOffset: new go.Point(0, 12),
              graduatedFunction: valueToDate
            },
            new go.Binding("interval", "length", calculateLabelInterval)
          )
        )
      );

      function calculateLabelInterval(len) {
        if (len >= 800) return 7;
        else if (400 <= len && len < 800) return 14;
        else if (200 <= len && len < 400) return 21;
        else if (140 <= len && len < 200) return 28;
        else if (110 <= len && len < 140) return 35;
        else return 365;
      }

      // The template for the link connecting the event node with the timeline bar node:
      myDiagram.linkTemplate =
        $(BarLink,  // defined below
          { toShortLength: 2, layerName: "Background" },
          $(go.Shape, { stroke: "#E37933", strokeWidth: 2 })
        );

      // Setup the model data -- an object describing the timeline bar node
      // and an object for each event node:
      var data = [
        { // this defines the actual time "Line" bar
          key: "timeline", category: "Line",
          lineSpacing: 30,  // distance between timeline and event nodes
          length: 700,  // the width of the timeline
          start: new Date("1 Jan 2016"),
          end: new Date("31 Dec 2016")
        },

        // the rest are just "events" --
        // you can add as much information as you want on each and extend the
        // default nodeTemplate to show as much information as you want
        { event: "New Year's Day", date: new Date("1 Jan 2016") },
        { event: "MLK Jr. Day", date: new Date("18 Jan 2016") },
        { event: "Presidents Day", date: new Date("15 Feb 2016") },
        { event: "Memorial Day", date: new Date("30 May 2016") },
        { event: "Independence Day", date: new Date("4 Jul 2016") },
        { event: "Labor Day", date: new Date("5 Sep 2016") },
        { event: "Columbus Day", date: new Date("10 Oct 2016") },
        { event: "Veterans Day", date: new Date("11 Nov 2016") },
        { event: "Thanksgiving", date: new Date("24 Nov 2016") },
        { event: "Christmas", date: new Date("25 Dec 2016") }
      ];

      // prepare the model by adding links to the Line
      for (var i = 0; i < data.length; i++) {
        var d = data[i];
        if (d.key !== "timeline") d.parent = "timeline";
      }

      myDiagram.model = $(go.TreeModel, { nodeDataArray: data });
    }

    function timelineDays() {
      var timeline = myDiagram.model.findNodeDataForKey("timeline");
      var startDate = timeline.start;
      var endDate = timeline.end;

      function treatAsUTC(date) {
        var result = new Date(date);
        result.setMinutes(result.getMinutes() - result.getTimezoneOffset());
        return result;
      }

      function daysBetween(startDate, endDate) {
        var millisecondsPerDay = 24 * 60 * 60 * 1000;
        return (treatAsUTC(endDate) - treatAsUTC(startDate)) / millisecondsPerDay;
      }
      return daysBetween(startDate, endDate);
    }

    // This custom Layout locates the timeline bar at (0,0)
    // and alternates the event Nodes above and below the bar at
    // the X-coordinate locations determined by their data.date values.
    function TimelineLayout() {
      go.Layout.call(this);
    };
    go.Diagram.inherit(TimelineLayout, go.Layout);

    TimelineLayout.prototype.doLayout = function(coll) {
      var diagram = this.diagram;
      if (diagram === null) return;
      coll = this.collectParts(coll);
      diagram.startTransaction("TimelineLayout");

      var line = null;
      var parts = [];
      var it = coll.iterator;
      while (it.next()) {
        var part = it.value;
        if (part instanceof go.Link) continue;
        if (part.category === "Line") { line = part; continue; }
        parts.push(part);
        var d = part.data.date;
        if (d === undefined) { d = new Date(); part.data.date = d; }
      }
      if (!line) throw Error("No node of category 'Line' for TimelineLayout");

      line.location = new go.Point(0, 0);

      // lay out the events above the timeline
      if (parts.length > 0) {
        // determine the offset from the main shape to the timeline's boundaries
        var main = line.findMainElement();
        var sw = main.strokeWidth;
        var mainOffX = main.actualBounds.x;
        var mainOffY = main.actualBounds.y;
        // spacing is between the Line and the closest Nodes, defaults to 30
        var spacing = line.data.lineSpacing;
        if (!spacing) spacing = 30;
        for (var i = 0; i < parts.length; i++) {
          var part = parts[i];
          var bnds = part.actualBounds;
          var dt = part.data.date;
          var val = dateToValue(dt);
          var pt = line.graduatedPointForValue(val);
          var tempLoc = new go.Point(pt.x, pt.y - bnds.height / 2 - spacing);
          // check if this node will overlap with previously placed events, and offset if needed
          for (var j = 0; j < i; j++) {
            var partRect = new go.Rect(tempLoc.x, tempLoc.y, bnds.width, bnds.height);
            var otherLoc = parts[j].location;
            var otherBnds = parts[j].actualBounds;
            var otherRect = new go.Rect(otherLoc.x, otherLoc.y, otherBnds.width, otherBnds.height);
            if (partRect.intersectsRect(otherRect)) {
              tempLoc.offset(0, -otherBnds.height - 10);
              j = 0; // now that we have a new location, we need to recheck in case we overlap with an event we didn't overlap before
            }
          }
          part.location = tempLoc;
        }
      }

      diagram.commitTransaction("TimelineLayout");
    };
    // end TimelineLayout class

    // This custom Link class was adapted from several of the samples
    function BarLink() {
      go.Link.call(this);
    }
    go.Diagram.inherit(BarLink, go.Link);

    BarLink.prototype.getLinkPoint = function(node, port, spot, from, ortho, othernode, otherport) {
      var r = port.getDocumentBounds();
      var op = otherport.getDocumentPoint(go.Spot.Center);
      var main = node.category === "Line" ? node.findMainElement() : othernode.findMainElement();
      var mainOffY = main.actualBounds.y;
      var y = r.top;
      if (node.category === "Line") {
        y += mainOffY;
        if (op.x < r.left) return new go.Point(r.left, y);
        if (op.x > r.right) return new go.Point(r.right, y);
        return new go.Point(op.x, y);
      } else {
        return new go.Point(r.centerX, r.bottom);
      }
    };
    // end BarLink class

    function valueToDate(n) {
      var timeline = myDiagram.model.findNodeDataForKey("timeline");
      var startDate = timeline.start;
      var startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000;
      var msPerDay = 24 * 60 * 60 * 1000;
      var date = new Date(startDateMs + n * msPerDay);
      return date.toLocaleDateString();
    }

    function dateToValue(d) {
      var timeline = myDiagram.model.findNodeDataForKey("timeline");
      var startDate = timeline.start;
      var startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000;
      var dateInMs = d.getTime() + d.getTimezoneOffset() * 60000;
      var msSinceStart = dateInMs - startDateMs;
      var msPerDay = 24 * 60 * 60 * 1000;
      return msSinceStart / msPerDay;
    }
  </script>
</head>
<body onload="init();">
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px black; background: #252526; width:100%; height:400px"></div>
  <p>
    This sample demonstrates an example usage of a <a href="../intro/graduatedPanels.html">Graduated Panel</a> to draw ticks and text labels along a timeline.
  </p>
  <p>
    The Panel uses a <a>Panel.graduatedTickUnit</a> of 1 to represent one day, and ticks are drawn at <a>Shape.interval</a>s of 7 to represent weeks.
  </p>
  <p>
    Labels are drawn at <a>TextBlock.interval</a>s of 14, or every two weeks. As the timeline is resized, the interval is updated to prevent overlaps.
    Text strings are generated by setting the <a>TextBlock.graduatedFunction</a>to convert from values in the graduated range to date strings.
    Also notice that labels use the <a>GraphObject.alignmentFocus</a>, <a>GraphObject.segmentOrientation</a>,
    and <a>GraphObject.segmentOffset</a> properties to place text below the timeline bar.
  </p>
  <p>
    Try resizing the timeline: select the timeline and drag the resize handle that is on the right side. Event nodes
    will automatically be laid out relative to the timeline using the <code>TimelineLayout</code>. TimelineLayout converts
    a date to a value, then uses <a>Panel.graduatedPointForValue</a> to help determine where event nodes will be placed.
  </p>
</div>
</body>
</html>